#include <retro.h>
#include "player.h"
#include "player.sprite.h"
#include "player.hittest.h"
#include "player.frame.h"

#define DIRECTIONS 4

using namespace Retro::Player;
using namespace Retro::Player::Graphic;

INL VOID GetGridMapSibling(
  Geom::Point& ioIndex,
  Direction iDir
  )
{
  switch (iDir)
  {
  case DirectionLeft:
    --ioIndex.x;
    break;
  case DirectionRight:
    ++ioIndex.x;
    break;
  case DirectionUp:
    --ioIndex.y;
    break;
  case DirectionDown:
    ++ioIndex.y;
    break;
  }
}

INL BOOL IsGridMapTileWalkable(
  const Geom::Point& iIndex,
  const Data::GridMap* iMap,
  const Data::Texture* iTileset,
  UINT iLayer,
  Direction iDir)
{
  if (iIndex.x < 0 || iIndex.x >= iMap->getWidth() ||
      iIndex.y < 0 || iIndex.y >= iMap->getHeight())
  {
    return FALSE;
  }

  INT wTexture = iMap->getGrid()[iLayer][
    iIndex.x + iIndex.y * iMap->getWidth()].texture;

  if (wTexture == -1 || !iTileset->getTileset())
    return TRUE;

  Data::TileData& wData = iTileset->getTileset()->data[wTexture];

  switch (iDir)
  {
  case DirectionLeft:
    return wData.flags & TILEDATA_LEFT;
  case DirectionRight:
    return wData.flags & TILEDATA_RIGHT;
  case DirectionUp:
    return wData.flags & TILEDATA_UP;
  case DirectionDown:
    return wData.flags & TILEDATA_DOWN;
  case DirectionNone:
    return wData.flags & TILEDATA_ALL;
  }

  return FALSE;
}

INL Direction InvertDirection(Direction iValue)
{
  switch (iValue)
  {
  case DirectionLeft:
    return DirectionRight;
  case DirectionRight:
    return DirectionLeft;
  case DirectionUp:
    return DirectionDown;
  case DirectionDown:
    return DirectionUp;
  }

  return DirectionNone;
}

INL VOID VelocityToDirection(
  const Vector::Vector2D& iVel,
  Direction& iX,
  Direction& iY
  )
{
  if (!iVel.x)
    iX = DirectionNone;
  else
    iX = iVel.x > 0 ? DirectionRight : DirectionLeft;

  if (!iVel.y)
    iY = DirectionNone;
  else
    iY = iVel.y > 0 ? DirectionDown : DirectionUp;
}

SpriteBuffer::SpriteBuffer()
{
  mBuffer.setSize(1000);
}

VOID  SpriteBuffer::init(Display::DisplayDevice* iDevice)
{
}

VOID  SpriteBuffer::uninit(Display::DisplayDevice* iDevice)
{
}

BOOL  SpriteBuffer::render(
  Display::DisplayDevice* iDevice,
  const Geom::Point& iOffset,
  const SpriteSet* iSpriteSet,
  UINT iStart,
  UINT iEnd,
  SpritePart iPart
  ) const
{
  Geom::Bounds wScreen = { -iOffset.x, -iOffset.y,
    iDevice->getWidth(), iDevice->getHeight()
  };

  if (iEnd >= mBuffer.getLength())
    iEnd = mBuffer.getLength() - 1;

  if (iStart < 0)     iStart = 0;
  if (iStart > iEnd)  iStart = iEnd;

  for (UINT i = iStart; i <= iEnd; ++i)
  {
    if (wScreen.intersects(mBuffer[i].bounds))
    {
      FCHECK(iSpriteSet->render(iDevice, &mBuffer[i], iOffset, iPart));
    }
  }

  return TRUE;
}

INT SpriteBuffer::hitTestMap(
  UINT iIndex,
  const Data::GridMap* iMap,
  UINT iLayer,
  HitTest::HitTestData& oData
  )
{
  Data::Texture* wTileset = (Data::Texture*)
    Frame::get()->getProject().findResource(
    Data::ResourceTexture, iMap->getTilesetID());

  Sprite& wSource = getSprite(iIndex);
  INT wTileSize   = wTileset->getDataTileset().tileSize;
  INT wIndex      = 0;

  Geom::Bounds  wBBox(wSource.bbox + wSource.bounds.point);

  Geom::Point   wStart = {
    (wSource.bounds.x + wSource.bbox.x) / wTileSize - 1,
    (wSource.bounds.y + wSource.bbox.y) / wTileSize - 1
  };

  Geom::Point   wEnd = {
    (LONG)ceilf((wSource.bounds.x + wSource.bbox.x + wSource.bbox.cx) /
    (REAL)wTileSize) + 1,
    (LONG)ceilf((wSource.bounds.y + wSource.bbox.y + wSource.bbox.cy) /
    (REAL)wTileSize) + 1
  };

  Geom::Point   wCurrentIndex = {
    (wSource.bounds.x + wSource.bbox.x + wSource.bbox.cx / 2) / wTileSize,
    (wSource.bounds.y + wSource.bbox.y + wSource.bbox.cy / 2) / wTileSize
  };

  Geom::Bounds  wCurrentTile = {
    wCurrentIndex.x * wTileSize,
    wCurrentIndex.y * wTileSize,
    wTileSize,
    wTileSize
  };

  Direction wDirX, wDirY;

  VelocityToDirection(wSource.vel, wDirX, wDirY);

  for (LONG x = wStart.x; x < wEnd.x; ++x)
  for (LONG y = wStart.y; y < wEnd.y; ++y)
  {
    Geom::Point   wIndex = {
      x,
      y
    };
    Geom::Bounds  wTile = {
      x * wTileSize,
      y * wTileSize,
      wTileSize,
      wTileSize
    };

    HitTest::HitTestData wHitTest = { wSource.vel };
    HitTest::HitTestBounds(wBBox, wTile, wHitTest);

    if (wHitTest.dirX != DirectionNone)
    {
      Geom::Point wSibling = { x, y };
      Direction wHitDir = InvertDirection(wHitTest.dirX);

      GetGridMapSibling(wSibling, wHitTest.dirX);

      if (IsGridMapTileWalkable(wSibling, iMap, wTileset, iLayer, wHitDir))
      {
        if (!IsGridMapTileWalkable(
          wIndex, iMap, wTileset, iLayer,
          InvertDirection(wDirX)))
        {
          wSource.vel.x = wHitTest.vel.x;
        }
      }
    }

    if (wHitTest.dirY != DirectionNone)
    {
      Geom::Point wSibling = { x, y };
      Direction wHitDir = InvertDirection(wHitTest.dirY);

      GetGridMapSibling(wSibling, wHitTest.dirY);

      if (IsGridMapTileWalkable(wSibling, iMap, wTileset, iLayer, wHitDir))
      {
        if (!IsGridMapTileWalkable(
          wIndex, iMap, wTileset, iLayer,
          InvertDirection(wDirY)))
        {
          wSource.vel.y = wHitTest.vel.y;
        }
      }
    }

    if (wHitTest.dirX != DirectionNone || wHitTest.dirY != DirectionNone)
    {
      wIndex = x + y * iMap->getWidth();
      oData = wHitTest;
    }
    else if (wHitTest.hit)
    {
      if (wDirX == DirectionLeft && wBBox.x <= wTile.x &&
          !IsGridMapTileWalkable(wIndex, iMap, wTileset, iLayer, wDirX))
      {
        wSource.vel.x = (REAL)wTile.x -
          (wSource.bounds.x + wSource.bbox.x);
      }
      else if (wDirX == DirectionRight &&
          wBBox.getRight() >= wTile.getRight() &&
          !IsGridMapTileWalkable(wIndex, iMap, wTileset, iLayer, wDirX))
      {
        wSource.vel.x = (REAL)wTile.getRight() -
          wBBox.cx - wSource.bounds.x - wSource.bbox.x;
      }

      if (wDirY == DirectionUp && wBBox.y <= wTile.y &&
          !IsGridMapTileWalkable(wIndex, iMap, wTileset, iLayer, wDirY))
      {
        wSource.vel.y = (REAL)wTile.y -
          (wSource.bounds.y + wSource.bbox.y);
      }
      else if (wDirY == DirectionDown &&
          wBBox.getBottom() >= wTile.getBottom() &&
          !IsGridMapTileWalkable(wIndex, iMap, wTileset, iLayer, wDirY))
      {
        wSource.vel.y = (REAL)wTile.getBottom() -
          wBBox.cy - wSource.bounds.y - wSource.bbox.y;
      }
    }
  }

  return wIndex;
}

INT SpriteBuffer::hitTestSprite(
  UINT iIndex,
  HitTest::HitTestData& oData
  )
{
  Sprite& wSource = getSprite(iIndex);
  INT wHitIndex = -1;
  HitTest::HitTestData wHitTest;

  for (UINT i = 0; i < getSize(); ++i)
  {
    if (i == iIndex)
      continue;

    Sprite& wTarget = getSprite(i);

    wHitTest.vel = wSource.vel;

    HitTest::HitTestBounds(
      wSource.bbox + wSource.bounds.point,
      wTarget.bbox + wTarget.bounds.point,
      wHitTest);

    wSource.vel = wHitTest.vel;

    if ((wHitTest.dirX != DirectionNone || wHitTest.dirY != DirectionNone) &&
      (wHitIndex == -1 || wHitTest.dist < oData.dist))
    {
      oData = wHitTest;
      wHitIndex = i;
    }
  }

  return wHitIndex;
}

SpriteSet::SpriteSet()
{
  mTexture    = NULL;
  mFrames     = 1;
  mSpriteSize = 0;
}

VOID  SpriteSet::init(Display::DisplayDevice* iDevice)
{
}

VOID  SpriteSet::uninit(Display::DisplayDevice* iDevice)
{
}

VOID  SpriteSet::getSource(
  const Sprite* iSprite,
  Geom::Bounds& oSource
  ) const
{
  Geom::Size wCount = {
    mTexture->getWidth() / mSpriteSize / mFrames,
    mTexture->getHeight() / mSpriteSize / DIRECTIONS
  };
  Geom::Point wTile = {
    iSprite->index / wCount.cx,
    iSprite->index % wCount.cx
  };

  oSource = Geom::Bounds::get(
    ((wTile.x * mFrames) + ((iSprite->frame / 100)) % mFrames) * mSpriteSize,
    ((wTile.y * DIRECTIONS) + (iSprite->dir % DIRECTIONS)) * mSpriteSize,
    mSpriteSize,
    mSpriteSize);
}

BOOL  SpriteSet::render(
  Display::DisplayDevice* iDevice,
  const Sprite* iSprite,
  const Geom::Point& iOffset,
  SpritePart iPart
  ) const
{
  if (mFrames == 0 || mSpriteSize == 0 ||
    !mTexture || mTexture->getWidth() == 0 || mTexture->getHeight() == 0)
    return FALSE;

  Geom::Bounds wSource;
  Geom::Bounds wDest = iSprite->bounds + iOffset;

  getSource(iSprite, wSource);

  switch (iPart)
  {
  case SpriteBody:
    wSource.y += wSource.cy / 2;
    wDest.y += wDest.cy / 2;
  case SpriteHead:
    wSource.cy /= 2;
    wDest.cy /= 2;
    break;
  }

  iDevice->beginBatch(mTexture);
  return iDevice->addBatch(wDest, &wSource);
}

